feat(dashboard) :主题切换支持跟随系统#8648
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The logic for resolving
uiThemefrom aThemeMode(resolveUiThemeFromModeinstores/customizer.tsandresolveUiThemeinconfig.ts) is currently duplicated; consider centralizing this into a single shared helper to avoid divergence in future changes. - The theme-switch UI (options array, current icon computation, and dropdown markup) is implemented separately in
VerticalHeader.vue,LoginPage.vue, andSetupPage.vue; extracting a reusableThemeSwitchercomponent would reduce duplication and make future behavior changes easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The logic for resolving `uiTheme` from a `ThemeMode` (`resolveUiThemeFromMode` in `stores/customizer.ts` and `resolveUiTheme` in `config.ts`) is currently duplicated; consider centralizing this into a single shared helper to avoid divergence in future changes.
- The theme-switch UI (options array, current icon computation, and dropdown markup) is implemented separately in `VerticalHeader.vue`, `LoginPage.vue`, and `SetupPage.vue`; extracting a reusable `ThemeSwitcher` component would reduce duplication and make future behavior changes easier to maintain.
## Individual Comments
### Comment 1
<location path="dashboard/src/stores/customizer.ts" line_range="6-15" />
<code_context>
const DARK_THEMES: ReadonlySet<string> = new Set(['PurpleThemeDark']);
-export const useCustomizerStore = defineStore("customizer", {
+function resolveUiThemeFromMode(mode: ThemeMode): string {
+ if (mode === 'dark') return 'PurpleThemeDark';
+ if (mode === 'light') return 'PurpleTheme';
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating theme-mode-to-uiTheme resolution logic between config and store.
`resolveUiTheme` in `config.ts` and `resolveUiThemeFromMode` here both map `ThemeMode` → `uiTheme`. Maintaining two mappings risks them diverging as themes evolve. Consider exporting a single helper (from `config.ts` or a shared theme util) and reusing it in the store so config and runtime logic share the same mapping.
Suggested implementation:
```typescript
import { defineStore } from 'pinia';
import config, { type ThemeMode, resolveUiTheme } from '@/config';
```
```typescript
const DARK_THEMES: ReadonlySet<string> = new Set(['PurpleThemeDark']);
const resolveUiThemeFromMode = resolveUiTheme;
```
</issue_to_address>
### Comment 2
<location path="dashboard/src/config.ts" line_range="1" />
<code_context>
+export type ThemeMode = 'light' | 'dark' | 'system';
+
export type ConfigProps = {
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the theme mode and uiTheme resolution (including localStorage and matchMedia handling) into a shared utility and letting this config file simply consume it.
You can reduce complexity and duplication by centralizing the `ThemeMode` + `uiTheme` logic in a small utility and keeping this config file “dumb” (just consuming that utility).
### 1. Extract theme mode utilities
Create a shared utility, e.g. `themeModeUtils.ts`:
```ts
// themeModeUtils.ts
export type ThemeMode = 'light' | 'dark' | 'system';
export function checkThemeMode(local: Storage): ThemeMode {
const mode = local.getItem('themeMode') as ThemeMode | null;
if (mode === 'light' || mode === 'dark' || mode === 'system') return mode;
const legacyTheme = local.getItem('uiTheme');
if (legacyTheme === 'PurpleThemeDark') {
local.setItem('themeMode', 'dark');
return 'dark';
}
if (legacyTheme === 'PurpleTheme') {
local.setItem('themeMode', 'light');
return 'light';
}
local.setItem('themeMode', 'system');
return 'system';
}
export function resolveUiTheme(mode: ThemeMode, prefersDark: boolean): string {
if (mode === 'dark') return 'PurpleThemeDark';
if (mode === 'light') return 'PurpleTheme';
return prefersDark ? 'PurpleThemeDark' : 'PurpleTheme';
}
export function resolveThemeState(local: Storage, win: Window | undefined) {
const themeMode = checkThemeMode(local);
const prefersDark =
typeof win !== 'undefined' &&
win.matchMedia('(prefers-color-scheme: dark)').matches;
const uiTheme = resolveUiTheme(themeMode, prefersDark);
local.setItem('uiTheme', uiTheme);
return { themeMode, uiTheme };
}
```
This keeps all existing behavior (migration, `matchMedia`, `localStorage` side effects) in one place.
### 2. Simplify `config` to just consume the utility
Then your config file becomes simpler and has no duplicated logic:
```ts
// config.ts
import { ThemeMode, resolveThemeState } from './themeModeUtils';
export type ConfigProps = {
Sidebar_drawer: boolean;
Customizer_drawer: boolean;
mini_sidebar: boolean;
fontTheme: string;
uiTheme: string;
themeMode: ThemeMode;
inputBg: boolean;
};
const { themeMode, uiTheme } = resolveThemeState(localStorage, window);
const config: ConfigProps = {
Sidebar_drawer: true,
Customizer_drawer: false,
mini_sidebar: false,
fontTheme: 'Roboto',
uiTheme,
themeMode,
inputBg: false,
};
export default config;
```
### 3. Reuse the same utility in the store
In `stores/customizer.ts`, replace any `resolveUiThemeFromMode` / duplicated `matchMedia` logic with calls to `resolveThemeState` / `resolveUiTheme`. For example:
```ts
// stores/customizer.ts (sketch)
import { resolveUiTheme, ThemeMode } from '../themeModeUtils';
function someSetter(mode: ThemeMode) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const uiTheme = resolveUiTheme(mode, prefersDark);
// ...
}
```
This removes duplicated mapping and `matchMedia` code, reduces cognitive load, and guarantees config and store stay in sync while preserving all current features.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request introduces support for a system theme mode ('light' | 'dark' | 'system') across the dashboard, updating the configuration, Pinia store, localization files, main entry point, and authentication pages. The review feedback focuses on enhancing environment safety by guarding localStorage and window.matchMedia calls for non-browser environments (like SSR or testing), reducing code duplication by reusing the resolveUiTheme helper in the store, and improving UI consistency by standardizing the system theme icon to mdi-theme-light-dark across all views.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
原主题切换为单按钮二态切换(亮/暗),无法选择跟随系统。改为与语言切换菜单一致的子菜单交互,支持浅色、深色、跟随系统三个选项。
Modifications / 改动点
config.ts:新增ThemeMode类型和themeMode字段,初始化时自动迁移旧uiTheme数据stores/customizer.ts:state 新增themeMode,新增SET_THEME_MODEactionmain.ts:提取setupThemeSync,注册全局唯一matchMedia监听器,system 模式下系统切换时自动更新主题VerticalHeader.vue:主题入口改为 hover 展开子菜单LoginPage.vue/SetupPage.vue:主题按钮改为点击展开下拉菜单i18n(6 个文件):新增system、title翻译键(en-US / zh-CN / ru-RU)Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in
requirements.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Add a configurable theme mode with light, dark, and system options and centralize theme synchronization with system preferences.
New Features:
Enhancements: